%% proxy behaviour with round robin backend choice

-module(gproxy2).
-export([start_be/1,start_gw/1,stop/1,rpc/2, change_code/2]).
%% backends management
-export([rrobin/1, change_elect/2, add_be/2, del_be/2, list_be/1]).

-define(TIMEOUT, 3000).

%% TODO: 
%%  - balance: gloop([S1, S2, S3])
%%  - backend failover

%% simple round robin rotation
rrobin(Pool) ->
  if Pool /= [] ->
    [First|TheRest] = Pool,
    {First,lists:flatten(TheRest,[First])};
  true ->
    {empty,[]}
  end.

%% start the server (backend)
start_be(Fun) ->
  spawn(fun() -> loop(Fun) end).

%% start the gateway (frontend)
%% Pool - a list of possible backends
start_gw(Pool) ->
  spawn(fun() -> gloop(Pool,fun(X)->gproxy2:rrobin(X) end) end).

%% stop gateway or backend
stop(Pid) -> 
  Pid ! stop.

%% Pid can be gateway or server 
rpc(Pid, Query) ->
  Pid ! {self(), Query},
  receive
    {Pid, crash} ->
      exit(rpc); 
    {Pid, mustwait} ->
      io:format("~p not ready. please wait...~n",[Pid]),
      exit(rpc); 
    {Pid, Reply} ->
      Reply
  end.

log_msg(Type, Pid, Query, Why) ->
  case Type of
    err ->
      io:format("[E] Server: ~p, Query: ~p, Reason: ~p~n", [Pid, Query, Why]);
    done ->
      io:format("[*] Done. Server: ~p, Query: ~p, Reply: ~p~n", [Pid, Query, Why]);
    timeout ->
      io:format("[T] Server: ~p, Timeout after: ~p~n", [Pid, Query])
  end.

%% function code change
%% send to the SERVER
change_code(Server, Fun) ->
  Server ! {swap_code,Fun}.

change_elect(Gw, Elect) ->
  Gw ! {swap_elect,Elect}.

add_be(Gw, Server) ->
  Gw ! {be_add, Server}.

del_be(Gw, Server) ->
  Gw ! {be_del, Server}.

list_be(Gw) ->
  Gw ! {self(), list_be},
  receive
    {ok,Reply} -> 
      Reply
  end.

%% server loop
loop(Fun) ->
  receive
    stop ->
      void;
    {swap_code,Fun1} ->
      loop(Fun1);
    %% direct request
    {Client, Data} ->
      case catch Fun(Data) of
        {'EXIT', Why} ->
          log_msg(err, Client, Data, Why),
          Client ! {self(), crash},
          loop(Fun);
        Reply ->
          Client ! {self(), Reply},
          loop(Fun)
      end;
    %% request via gateway
    {Gw, Client, Data} ->
      case catch Fun(Data) of
        {'EXIT', Why} ->
          log_msg(err, Client, Data, Why),
          Gw ! {self(), Client, crash},
          loop(Fun);
        Reply ->
          Gw ! {self(), Client, ok, Reply},
          loop(Fun)
      end
  end.

%% gateway loop
gloop(Pool,Elect) ->
  receive
    stop ->
      void;
    {swap_elect,Elect1} ->
      gloop(Pool, Elect1);
    {be_add,Server} ->
      gloop([Server|Pool], Elect);
    {be_del,Server} ->
      gloop(Pool, Elect);
    {Client,list_be} ->
      Client ! {ok, Pool},
      gloop(Pool, Elect);
    {Client, Data} ->
      if Pool /= [] -> 
        case catch Elect(Pool) of
          {empty,[]} ->
            Client ! {self(), mustwait},
            gloop(Pool, Elect);
          {'EXIT', Why} ->
            Client ! {self(), mustwait},
            gloop(Pool, Elect);
          {Backend,Pool1} ->
            Backend ! {self(), Client, Data},
            gloop(Pool1,Elect)
        end;
       true ->  %% empty pool
         Client ! {self(), mustwait},
         gloop(Pool, Elect)
       end;
    {Server, Client, crash} ->
      Client ! {self(), crash},
      gloop(Pool, Elect);
    {Server, Client, ok, Data} ->
      Client ! {self(), Data},
      gloop(Pool, Elect)
  end.
